#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
    Part of Seagull - The Pyhon Pages engine
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Reflexion:
      The only module which a Python Page should access to gather
      information about the engine or other pages. This is mostly a
      proxy to other classes.
"""
__author__ = 'Jonathan Slenders <jonathan at slenders dot be>'
__license__ = 'GNU LGPL'

__all__ = ('Reflexion', 'PageProviderInfo', 'DirectoryInfo', 'PythonPageInfo',
		'BinaryPageInfo', 'PythonPageInfo', 'PageBuilder', 'PageNotFoundException')

import os
import sys
import xml.dom.minidom
import codecs
import platform

import compiler.parser
import renderer
import renderer_input
import page_providers
import page_compiler

from configuration import SystemConfig

def _class_from_page_provider_object(page_provider_object):
	""" For internal use only """
	if isinstance(page_provider_object, page_providers.PythonPage):
		return PythonPageInfo(page_provider_object)

	elif isinstance(page_provider_object, page_providers.BinaryPage):
		return BinaryPageInfo(page_provider_object)

	elif isinstance(page_provider_object, page_providers.Directory):
		return DirectoryInfo(page_provider_object)

	elif isinstance(page_provider_object, list): # List of Directories
		return DirectoryInfo(page_provider_object)

class Reflexion(object):
	"""
	Main reflexion class
	"""
	def __init__(self):
		pass

	@staticmethod
	def list_page_providers():
		"""
		Returns an iterator which yields PageProviderInfo objects for each
		page provider
		"""
		for provider in SystemConfig().page_providers:
			yield PageProviderInfo(provider)

	@staticmethod
	def list_pages(path=u'/'):
		"""
		Yields a list of all the pages of all the page providers.
		The result is in alphabetical order.
		"""
		assert(path.startswith('/'))
		pages = { }

		for provider in SystemConfig().page_providers:
			for p in provider.walk(path=path):
				if isinstance(p, page_providers.Directory):
					if not p.name in pages:
						pages[p.name] = [p]
					elif isinstance(pages[p.name], list):
						pages[p.name].append(p)
				elif not p.name in pages:
					pages[p.name] = p

		keys = pages.keys(); keys.sort()
		for k in keys:
			yield _class_from_page_provider_object(pages[k])

	@staticmethod
	def lookup_page(page_path):
		"""
		Returns a PythonPageInfo object for this page
		"""
		assert(page_path.startswith('/'))

		# Try for each provider
		for provider in SystemConfig().page_providers:
			try:
				page = provider.lookup_page(page_path)

				if isinstance(page, page_providers.PythonPage):
					return PythonPageInfo(page)
				elif isinstance(page, page_providers.BinaryPage):
					return BinaryPageInfo(page)
				else:
					raise PageNotFoundException(page_path)

			except page_providers.PageNotFoundException:
				pass

		raise PageNotFoundException(page_path)

	@staticmethod
	def list_filters():
		"""
		List the available output filters (for XHTML).
		Returns a dictionary which maps the filter name to its docstring.
		"""
		import filters # For some reason, the import needs to be done here
		filter_directory = filters.__path__[0]
		filters = { }
		for f in os.listdir(filter_directory):
			if f.endswith('.py') and f != '__init__.py':
				# Strip .py
				f = f[:-3]
				
				# Load filter docstring
				module = __import__('filters.%s' % f)
				filters[f] = getattr(module, f).__doc__
		return filters

	@staticmethod
	def create_directory(path):
		"""
		Create directory in every page provider which has
		the parent directory. (This can both be in binary and
		python page providers)
		"""
		assert path.startswith('/')

		# For each provider
		succes = False
		for provider in SystemConfig().page_providers:
			try:
				provider.create_directory(path)
				succes = True
			except page_providers.DirectoryNotFoundException:
				pass # And try next provider
			except NotImplementedError:
				pass # And try next provider

		if not succes:
			raise DirectoryCreationException('Could not create directory "%s" in at ' % path +
				'least one page provider')


class PageProviderInfo(object):
	"""
	Reflexion information about a Python Page Provider
	"""
	def __init__(self, page_provider):
		self.__page_provider = page_provider
		self.name = page_provider.name

	def list_pages(self, path=u'/'):
		"""
		Returns an iterator which yields PythonPageinfo objects.
		The result is always in alphabetical order
		"""
		assert(path.startswith('/'))
		pages = { }
		for p in self.__page_provider.walk(path=path):
			pages[p.name] = p

		keys = pages.keys(); keys.sort()
		for k in keys:
			yield _class_from_page_provider_object(pages[k])


class DirectoryInfo(object):
	"""
	Reflexion information for a web directory.
	"""
	def __init__(self, directory_objects):
		if not isinstance(directory_objects, list):
			directory_objects = [ directory_objects ]

		self.__directory_objects = directory_objects

		self.name = directory_objects[0].name
		self.path = directory_objects[0].path

	def list_pages(self):
		"""
		List the pages in this directory.
		The result is in alphabetical order.
		"""
		pages = { }

		for d in self.__directory_objects:
			for p in d.walk():
				if isinstance(p, page_providers.Directory):
					if not p.name in pages:
						pages[p.name] = [p]
					elif isinstance(pages[p.name], list):
						pages[p.name].append(p)
				elif not p.name in pages:
					pages[p.name] = p

		keys = pages.keys(); keys.sort()
		for k in keys:
			yield _class_from_page_provider_object(pages[k])
				
class PythonPageInfo(object):
	"""
	Reflexion information for a certain Python Page
	"""
	class Parameter(object):
		def __init__(self, name, type, classname, description, default_value):
			self.name = name
			self.type = type
			self.classname = classname
			self.description = description
			self.default_value = default_value

	class Placeholder(object):
		def __init__(self, name):
			self.name = name

	class IncludedPage(object):
		def __init__(self, varname, path, parameters):
			self.varname = varname
			self.path = path
			self.parameters = parameters

	class BreakPoint(object):
		def __init__(self, line, column):
			self.line = line
			self.column = column

	def __init__(self, page_object):
		"""
		path: unicode string, relative to ./pages/ and starting with a slash
		"""
		assert(isinstance(page_object, page_providers.PythonPage))
		self.__page_object = page_object

		self.directory = page_object.directory
		self.name = page_object.name
		self.path = page_object.path
		self.url_suffix = page_object.url_suffix
		self.path_and_suffix = page_object.path + page_object.url_suffix

		self.output_mode = None
		self.master = None
		self.debug = False
		self.placeholders = [ ]
		self.parameters = [ ]
		self.included_pages = [ ]
		self.breakpoints = [ ]
		self.external = False
		self.meta = { }

		try:
			self.__read_meta_data()

		except page_providers.NotCompiledException: # Not compiled?
			try:
				# Not compiled? Compile and try again!
				compiler = page_compiler.PageCompiler()
				compiler.compile_page(page_object)

				self.__read_meta_data()
			except:
				# Compilation failed
				pass

	def __read_meta_data(self):
		def get_info_node():
			# Get the <pageinfo> root node
			xmldoc = xml.dom.minidom.parseString(self.__page_object.get_meta_data())

			for e in xmldoc.childNodes:
				if e.nodeType == e.ELEMENT_NODE and e.localName == 'pageinfo':
					return e

		pageinfo = get_info_node()

		if pageinfo.hasAttribute('master'):
			self.master = pageinfo.getAttribute('master')

		if pageinfo.hasAttribute('output-mode'):
			self.output_mode = pageinfo.getAttribute('output-mode')

		if pageinfo.hasAttribute('external'):
			self.external = (pageinfo.getAttribute('external') == 'True')

		if pageinfo.hasAttribute('debug'):
			self.debug = (pageinfo.getAttribute('debug') == 'True')

		for e in pageinfo.childNodes:
			if e.nodeType == e.ELEMENT_NODE:
				try:
					if e.localName == 'placeholder':
						name = e.getAttribute('name')
						self.placeholders.append(self.Placeholder(name))
					elif e.localName == 'parameter':
						name = e.getAttribute('name')
						type = e.getAttribute('type')
						classname = e.getAttribute('class')
						description = e.getAttribute('description')
						default_value = e.getAttribute('default-value')
						self.parameters.append(
							self.Parameter(name, type, classname, description, default_value))
					elif e.localName == 'meta':
						name = e.getAttribute('name')
						value = e.getAttribute('value')
						self.meta[name] = value
					elif e.localName == 'breakpoint':
						line = e.getAttribute('line')
						column = e.getAttribute('column')
						self.breakpoints.append(
							self.BreakPoint(line, column))
					elif e.localName == 'included-page':
						path = e.getAttribute('path')
						varname = e.getAttribute('varname')
						parameters = { }

						for e2 in e.childNodes:
							if e2.nodeType == e2.ELEMENT_NODE and \
									e2.localName == 'parameter':
								name = e2.getAttribute('name')
								value = e2.getAttribute('value')
								parameters[name] = value

						self.included_pages.append(
							self.IncludedPage(varname, path, parameters))
				except:
					pass

	def get_source_code(self):
		return self.__page_object.get_source()
	
	def render(self, query_parameters={ }, debug_interrupt_callback=None, trace_callback=None):
		"""
		Renders this page. Returns a RendererResult object with
		the status_code, headers and body.
		IMPORTANT: Never ever try to render a page himself,
		           this can result in eternal recursion.
		"""
		class Output(renderer.OutputBuffer):
			def __init__(self):
				renderer.OutputBuffer.__init__(self)
				self.__status_code = None
				self.__headers = { }
				self.__body = [ ]

			def _write_headers_body(self, headers, status_code):
				self.__headers = headers
				self.__status_code = status_code

			def _write_content_body(self, string):
				self.__body.append(string)

			def get_status_code(self):
				return self.__status_code

			def get_headers(self):
				return self.__headers

			def get_content(self):
				return ''.join(self.__body)
			
		# Input and output objects
		out = Output()
		input = renderer_input.SimpleRenderInput(self.path, query_parameters)

		if debug_interrupt_callback:
			input.debug_interrupt = debug_interrupt_callback

		if trace_callback:
			input.trace_callback = trace_callback

		# Render and return output
		renderer.Renderer(input, out)
		return out

class BinaryPageInfo(object):
	"""
	Reflexion information for a certain Binary Page
	"""
	def __init__(self, page_object):
		assert(isinstance(page_object, page_providers.BinaryPage))

		self.name = page_object.name
		self.directory = page_object.directory
		self.path = page_object.path


class PageBuilder(object):
	"""
	Build a new Page
	"""
	def __init__(self):
		self._page = compiler.parser.PythonPage()

	def add_meta(self, key, value):
		""" Add meta info to the page """
		self._page.meta.append(compiler.parser.Meta(key, value))

	def add_content(self, name, content):
		""" Add content block (with this text) """
		content_block = compiler.parser.ContentBlock(name)
		content_block.content.append(content)
		self._page.content_blocks.append(content_block)

	def set_external(self, value):
		""" Set external property of the page """
		assert isinstance(value, bool)
		self._page.external = value

	def set_master(self, path, parameters={ }):
		""" Set master path of this page """
		assert isinstance(path, unicode)
		assert isinstance(parameters, dict)
		self.master = compiler.parser.Master(path, parameters)

	def save_to_path(self, path):
		""" Save this new page as a python page at this path """
		assert path[0] == '/'

		# Build source code from this new page
		import compiler.writeback	
		source_code = compiler.writeback.WriteBack(self._page).get_result()

		# Use writeback to create source file
		for provider in SystemConfig().page_providers:
			if isinstance(provider, page_providers.PythonPageProvider):
				try:
					provider.create_page(path, source_code)
					return
				except page_providers.PageExistsException, e:
					raise PageCreationException(e.message)
				except:
					pass # And try next provider

		raise PageCreationException('Could not save page to "%s", make sure at least one ' % path +
				'page provider contains this directory')


class PageCreationException(Exception):
	def __init__(self, message):
		Exception.__init__(self, message)

class DirectoryCreationException(Exception):
	def __init__(self, message):
		Exception.__init__(self, message)

class PageNotFoundException(Exception):
	def __init__(self, page_path):
		Exception.__init__(self, u'Page "%s" does not exist' % page_path)
